Skip to content

[linter-miner] linter: add sprintfint — flag fmt.Sprintf("%d", x) where x is int - #42538

Merged
pelikhan merged 5 commits into
mainfrom
linter-miner/sprintfint-450f52ab758a01dc
Jun 30, 2026
Merged

[linter-miner] linter: add sprintfint — flag fmt.Sprintf("%d", x) where x is int#42538
pelikhan merged 5 commits into
mainfrom
linter-miner/sprintfint-450f52ab758a01dc

Conversation

@github-actions

@github-actions github-actions Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a new custom Go static-analysis linter, sprintfint, that flags fmt.Sprintf("%d", x) calls where x has the exact predeclared type int and suggests replacing them with strconv.Itoa(x).

What Changed

File Change
pkg/linters/sprintfint/sprintfint.go New analyzer implementation
pkg/linters/sprintfint/sprintfint_test.go Unit test via analysistest
pkg/linters/sprintfint/testdata/src/sprintfint/sprintfint.go analysistest fixture with // want annotations
cmd/linters/main.go Register sprintfint.Analyzer in the multichecker
docs/adr/42538-add-sprintfint-linter.md ADR documenting the decision

Detection Logic

The analyzer walks ast.CallExpr nodes and reports a diagnostic when all of the following hold:

  • The callee is fmt.Sprintf (selector check via astutil.IsPkgSelector).
  • Exactly two arguments are present.
  • The first argument is the string literal "%d".
  • The second argument resolves to exactly types.Typ[types.Int] (plain int; named types, int64, uint, etc. are not flagged).

Skips _test.go files (filecheck.IsTestFile) and respects //nolint:sprintfint line directives.

Suggested Fix

buildItoaFix emits a single analysis.TextEdit rewriting the full call expression to strconv.Itoa(<arg>). Editors and go tool can auto-apply the fix; callers may need goimports to adjust imports afterward.

Scope Boundaries

  • Only the predeclared int type is covered. int64, int32, uint, and named integer types are intentionally out of scope.
  • Test files are excluded from diagnostics.

Context (from ADR-42538)

Two existing production call sites (pkg/stringutil/stringutil.go:59, pkg/console/console_wasm.go:41) use the suboptimal fmt.Sprintf("%d", x) idiom. This linter prevents regression after those sites are fixed. Alternatives considered: code-review-only enforcement (proven insufficient), external perfsprint linter (broader scope, breaks internal convention), and broader integer-type coverage (deferred due to type-specific suggestion complexity).

Commits

  • 18c8a69b4 linter: add sprintfint — flag fmt.Sprintf("%d", x) where x is int
  • 52f92c3e0 docs(adr): draft ADR-42538 for sprintfint linter addition
  • 0c271b511 chore: start pr finisher pass
  • 02539014c fix sprintfint review feedback

Generated by PR Description Updater for #42538 · 51.9 AIC · ⌖ 7.35 AIC · ⊞ 4.7K ·

Adds a new custom Go analysis linter, `sprintfint`, that reports calls of
the form `fmt.Sprintf("%d", x)` where `x` is a single value of type `int`.
These can be rewritten as `strconv.Itoa(x)`, which is simpler, avoids
format-string parsing at runtime, and makes the intent clearer.

## What it catches

```go
// flagged
s := fmt.Sprintf("%d", n)   // n is int

// preferred
s := strconv.Itoa(n)
```

## What it does NOT flag

- `fmt.Sprintf("%d", n64)`  — n64 is int64 (use strconv.FormatInt)
- `fmt.Sprintf("%d", u)`    — u is uint
- `fmt.Sprintf("%d %d", a, b)` — multiple verbs
- Calls suppressed with `//nolint:sprintfint`

## Implementation

- `pkg/linters/sprintfint/sprintfint.go` — analyzer
- `pkg/linters/sprintfint/sprintfint_test.go` — tests via analysistest
- `pkg/linters/sprintfint/testdata/src/sprintfint/sprintfint.go` — fixtures
- `cmd/linters/main.go` — registration in multichecker

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions github-actions Bot added automation cookie Issue Monster Loves Cookies! go-linters labels Jun 30, 2026
@pelikhan
pelikhan marked this pull request as ready for review June 30, 2026 18:31
Copilot AI review requested due to automatic review settings June 30, 2026 18:31
@github-actions

github-actions Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor Author

Design Decision Gate 🏗️ completed the design decision gate check.

@github-actions

github-actions Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor Author

PR Code Quality Reviewer completed the code quality review.

@github-actions

Copy link
Copy Markdown
Contributor Author

🧠 Matt Pocock Skills Reviewer is reviewing this pull request using Matt Pocock's engineering skills...

@github-actions

github-actions Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor Author

Test Quality Sentinel completed test quality analysis.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor Author

🏗️ Design Decision Gate — ADR Required

This PR makes significant changes to core business logic (174 new lines in pkg/) but does not have a linked Architecture Decision Record (ADR).

📄 Draft ADR committed: docs/adr/42538-add-sprintfint-linter.md — review and complete it before merging.

🔒 This PR cannot merge until an ADR is linked in the PR body.

📋 What to do next
  1. Review the draft ADR committed to your branch — it was generated from the PR diff
  2. Complete the missing sections — add context the AI could not infer, refine the decision rationale, and list real alternatives you considered
  3. Commit the finalized ADR to docs/adr/ on your branch
  4. Reference the ADR in this PR body by adding a line such as:

    ADR: ADR-42538: Add sprintfint Linter

Once an ADR is linked in the PR body, this gate will re-run and verify the implementation matches the decision.

❓ Why ADRs Matter

"AI made me procrastinate on key design decisions. Because refactoring was cheap, I could always say 'I'll deal with this later.' Deferring decisions corroded my ability to think clearly."

ADRs create a searchable, permanent record of why the codebase looks the way it does. Future contributors (and your future self) will thank you.

📋 Michael Nygard ADR Format Reference

An ADR must contain these four sections to be considered complete:

  • Context — What is the problem? What forces are at play?
  • Decision — What did you decide? Why?
  • Alternatives Considered — What else could have been done?
  • Consequences — What are the trade-offs (positive and negative)?

All ADRs are stored in docs/adr/ as Markdown files numbered by PR number (e.g., 42538-add-sprintfint-linter.md for PR #42538).

🏗️ ADR gate enforced by Design Decision Gate 🏗️ · 48.5 AIC · ⌖ 9.99 AIC · ⊞ 8.4K ·
Comment /review to run again

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: sprintfint linter

Good concept and clean structure. One correctness bug needs fixing before merge.

Blocking — correctness bug

argType.Underlying() matches named int types (sprintfint.go:77)

The check argType.Underlying() != types.Typ[types.Int] passes for any type whose underlying type is int, including user-defined named types like type Status int or type Counter int. The resulting SuggestedFix emits strconv.Itoa(s) which fails to compile when s is a named type — strconv.Itoa only accepts the exact predeclared int.

Fix: remove .Underlying():

if argType != types.Typ[types.Int] {
    return
}

Add a goodNamedInt fixture to testdata to prevent regression.

Non-blocking

  • main.go function body: sprintfint.Analyzer is registered before sprintferrorsnew.Analyzer, but alphabetical order (sprintferr... < sprintfi...) requires it to come after.
  • buildItoaFix does not manage strconv import addition or potential fmt import removal; advisory for tooling authors applying this fix.

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · 70.1 AIC · ⌖ 11.5 AIC · ⊞ 4.9K

Comment thread pkg/linters/sprintfint/sprintfint.go Outdated
if argType == nil {
return
}
if argType.Underlying() != types.Typ[types.Int] {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Underlying() causes false positives for named types based on int

The comment on line 71 says "exact type int", but argType.Underlying() also matches any named type whose underlying type is int, e.g.:

type Status int
var s Status = 5
fmt.Sprintf("%d", s) // ← flagged by this linter

The suggested fix strconv.Itoa(s) fails to compile because strconv.Itoa only accepts int, not Status. Use exact type equality instead:

if argType != types.Typ[types.Int] {
    return
}

This limits the linter to the exact predeclared int type and avoids broken auto-fix suggestions for named int types.

@copilot please address this.

Comment thread cmd/linters/main.go Outdated
seenmapbool.Analyzer,
sortslice.Analyzer,
sprintferrdot.Analyzer,
sprintfint.Analyzer,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: analyzer registration is out of alphabetical order

The imports (lines 47–49) are correctly sorted: sprintferrdotsprintferrorsnewsprintfint. But the registration in main() has sprintfint.Analyzer inserted before sprintferrorsnew.Analyzer, which reverses the correct order (sprintferr... < sprintfi... lexicographically).

Expected order:

sprintferrdot.Analyzer,
sprintferrorsnew.Analyzer,
sprintfint.Analyzer,

@copilot please address this.

// suppressed suppresses the linter directive — no diagnostic expected.
func suppressed(n int) string {
return fmt.Sprintf("%d", n) //nolint:sprintfint
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing test case: named type based on int

After fixing the Underlying() type check, add a test fixture that confirms named int types are not flagged:

// goodNamedInt uses a named type based on int — not flagged, because
// strconv.Itoa does not accept named types.
func goodNamedInt(n Counter) string {
    type Counter int  // or defined at package level
    return fmt.Sprintf("%d", n)
}

Without this, a future refactor that reintroduces Underlying() could silently re-break the behaviour.

@copilot please address this.

@github-actions

Copy link
Copy Markdown
Contributor Author

🧪 Test Quality Sentinel Report

Test Quality Score: 100/100 — Excellent

Analyzed 1 test(s): 1 design, 0 implementation, 0 guideline violation(s).

📊 Metrics & Test Classification (1 test analyzed)
Metric Value
New/modified tests analyzed 1
✅ Design tests (behavioral contracts) 1 (100%)
⚠️ Implementation tests (low value) 0 (0%)
Tests with error/edge cases 1 (100%)
Duplicate test clusters 0
Test inflation detected No (17 test lines / 109 production lines ≈ 0.16:1)
🚨 Coding-guideline violations 0
Test File Classification Issues Detected
TestSprintfInt pkg/linters/sprintfint/sprintfint_test.go:14 ✅ Design

Go: 1 (*_test.go); JavaScript: 0. Other languages detected but not scored.

Test coverage breakdownanalysistest.Run verifies 8 scenarios via // want annotations in the testdata fixture:

Scenario Kind Expected
bad()fmt.Sprintf("%d", n) where n is int Positive Flagged ✅
badLiteral()fmt.Sprintf("%d", 42) integer literal Positive Flagged ✅
goodStrconvItoa() — already uses strconv.Itoa Negative Not flagged ✅
goodInt64()int64 argument Negative/edge Not flagged ✅
goodUint()uint argument Negative/edge Not flagged ✅
goodMultipleVerbs()"%d %d" format Negative/edge Not flagged ✅
goodOtherVerb()"%v" verb Negative/edge Not flagged ✅
suppressed()//nolint:sprintfint directive Negative/edge Not flagged ✅

Verdict

Check passed. 0% implementation tests (threshold: 30%). The single test uses analysistest.Run with // want pattern annotations — the idiomatic approach for Go analysis linters — covering both positive (flagged) and negative (not-flagged) behavioral contracts across all documented exclusion conditions. Build tag //go:build !integration is correctly present on line 1. No mocking libraries. No test inflation.

References: §28467215764

🧪 Test quality analysis by Test Quality Sentinel · 62.2 AIC · ⌖ 12.3 AIC · ⊞ 6.7K ·
Comment /review to run again

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Test Quality Sentinel: 100/100. Test quality is acceptable — 0% of new tests are implementation tests (threshold: 30%).

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

REQUEST_CHANGES — two correctness defects in the analyzer + one fix completeness gap

Blocking issues found (fix required before merge)
  1. Broken SuggestedFix for named int types (sprintfint.go:77): argType.Underlying() causes the linter to fire on type MyInt int arguments, but the emitted fix strconv.Itoa(x) does not compile for named types. Change to argType != types.Typ[types.Int] (direct comparison) so only bare int is matched, or generate strconv.Itoa(int(x)) for named types.

  2. SuggestedFix doesn't add the strconv import (sprintfint.go:105): applying the fix in a file that doesn't already import "strconv" produces uncompilable code. Either handle the import TextEdit or document the limitation explicitly.

  3. Out-of-order Analyzer registration (cmd/linters/main.go:89): sprintfint.Analyzer is inserted between sprintferrdot and sprintferrorsnew; alphabetically it belongs after sprintferrorsnew.

  4. Missing named-type test fixture (testdata/.../sprintfint.go:48): no test guards the type MyInt int boundary, so the false-positive + broken-fix scenario goes undetected.

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • patchdiff.githubusercontent.com

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "patchdiff.githubusercontent.com"

See Network Configuration for more information.

🔎 Code quality review by PR Code Quality Reviewer · 117.1 AIC · ⌖ 8.13 AIC · ⊞ 1.6K
Comment /review to run again

Comment thread pkg/linters/sprintfint/sprintfint.go Outdated
if argType == nil {
return
}
if argType.Underlying() != types.Typ[types.Int] {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Underlying() match fires on named int types but generates uncompilable SuggestedFix: type MyInt int; fmt.Sprintf("%d", myIntVar) passes this guard (because MyInt.Underlying() == types.Typ[types.Int]), but the emitted fix strconv.Itoa(myIntVar) does not compile—strconv.Itoa only accepts bare int, not a named type.

💡 Suggested fix

Use a direct pointer comparison so only the bare int type is matched:

// Before — incorrectly matches named types derived from int
if argType.Underlying() != types.Typ[types.Int] {
    return
}

// After — only matches the exact built-in int type
if argType != types.Typ[types.Int] {
    return
}

If named int types should also be flagged in the future, the fix generator must emit strconv.Itoa(int(x)) (with the explicit conversion), not just strconv.Itoa(x).

{
Pos: call.Pos(),
End: call.End(),
NewText: []byte("strconv.Itoa(" + argText + ")"),

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SuggestedFix omits the strconv import, producing uncompilable code when applied in files that don't already import it: the TextEdit only rewrites the call site. Any file where "strconv" is not yet imported will fail to compile after the fix is applied.

💡 Suggested fix

Check whether strconv is already imported and add a TextEdit for the import declaration when it is not. Other analyzers in this repo (e.g. fprintlnsprintf) handle this via a helper that inserts the import block edit.

Alternatively, document the limitation clearly in Analyzer.Doc so that tool integrators know goimports / gopls must be run alongside the fix. As-is, a user running go vet -fix will silently get broken code.

Comment thread cmd/linters/main.go Outdated
seenmapbool.Analyzer,
sortslice.Analyzer,
sprintferrdot.Analyzer,
sprintfint.Analyzer,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sprintfint.Analyzer is registered out of alphabetical order: it appears between sprintferrdot and sprintferrorsnew, but alphabetically sprintferr* < sprintfint (since 'e' < 'i'). The import block in this same file is in the correct order (sprintferrorsnewsprintfintssljson); the main() slice should match.

💡 Suggested fix
// in main()
sprintferrdot.Analyzer,
sprintferrorsnew.Analyzer,  // move before sprintfint
sprintfint.Analyzer,

// suppressed suppresses the linter directive — no diagnostic expected.
func suppressed(n int) string {
return fmt.Sprintf("%d", n) //nolint:sprintfint
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing test case for named int types: the fixture has no case like type MyInt int; fmt.Sprintf("%d", myIntVar), which is the exact scenario where the Underlying()-based type check produces a false positive and the generated SuggestedFix emits uncompilable code. Add a // want -free case (or an explicit no-diagnostic marker) to catch regressions on this boundary once the type check is corrected.

💡 Suggested addition
type myInt int

// goodNamedInt is a named type; the linter must NOT flag this because the
// suggested fix strconv.Itoa(x) would not compile for a non-int type.
func goodNamedInt(n myInt) string {
    return fmt.Sprintf("%d", n) // no diagnostic expected
}

@pelikhan

Copy link
Copy Markdown
Collaborator

@copilot run pr-finisher skill

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>

Copilot AI commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

@copilot run pr-finisher skill

Ran the PR finisher pass. I addressed the sprintfint review feedback in 0253901 and verified make fmt, make lint, make test-unit, and make test locally. CI on this new head still needs a maintainer re-trigger to verify the pushed commit.

Copilot AI requested a review from pelikhan June 30, 2026 19:17
@pelikhan
pelikhan merged commit 34a5b34 into main Jun 30, 2026
29 checks passed
@pelikhan
pelikhan deleted the linter-miner/sprintfint-450f52ab758a01dc branch June 30, 2026 19:48
@github-actions

Copy link
Copy Markdown
Contributor Author

🎉 This pull request is included in a new release.

Release: v0.82.1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

automation cookie Issue Monster Loves Cookies! go-linters

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants